package main

import (
	"context"
	"errors"
	"fmt"
	"gitee.com/kaylee595/gkaylee/kerr"
	"github.com/gdamore/tcell/v2"
	"github.com/imroc/req/v3"
	"github.com/lmittmann/tint"
	"github.com/rivo/tview"
	"github.com/samber/lo"
	"log/slog"
	"net/http"
	url2 "net/url"
	"os"
	"regexp"
	"strings"
	"yyLogin/pkg/baiduCaptchaV2Resolve"
	"yyLogin/pkg/jfbym"
	"yyLogin/pkg/vipyy"
)

const defaultUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"

var httpClient = req.C().ImpersonateChrome().SetCommonHeader("User-Agent", defaultUA)

var stdout = tview.NewTextView().SetDynamicColors(true).SetRegions(true)

func init() {
	initLog()
}

// initLog 初始化日志
func initLog() {
	const errKey = "err"
	handler := tint.NewHandler(tview.ANSIWriter(stdout), &tint.Options{
		AddSource:  true,
		Level:      slog.LevelDebug,
		TimeFormat: "01-02 15:04:05",
		ReplaceAttr: func(groups []string, attr slog.Attr) slog.Attr {
			if attr.Key == errKey {
				err, ok := attr.Value.Any().(error)
				if !ok {
					return attr
				}
				stacks := kerr.GetTraceStacks(err)
				if len(stacks) > 0 { // 错误带跟踪栈格式化栈内容
					traces := lo.Reduce(stacks, func(agg string, item kerr.TraceStack, index int) string {
						filename := item.FileName
						nodes := strings.Split(filename, "/")
						if len(nodes) > 2 {
							l := len(nodes)
							if nodes[l-2] == "runtime" {
								return agg
							}
							filename = nodes[l-2] + "/" + nodes[l-1]
						}
						data := fmt.Sprintf("%s:%d", filename, item.Line)
						if index == 0 {
							return data
						}
						return agg + " <- " + data
					}, "")
					// 将信息包装在分组中
					attr.Value = slog.GroupValue(
						tint.Err(err),
						slog.Any("traces", traces),
					)
					return attr
				}
				return tint.Err(err)
			}
			return attr
		},
	})
	slog.SetDefault(slog.New(handler))
}

func main() {
	var username, password string
	app := tview.NewApplication()
	stdout.SetBorder(true)
	stdout.SetChangedFunc(func() {
		app.Draw()
	})
	form := tview.NewForm()
	form.SetBorder(true).SetTitleAlign(tview.AlignLeft)
	form.AddInputField("YY账户", "", 15, nil, func(v string) { username = v }).
		AddPasswordField("YY密码", "", 15, '*', func(v string) { password = v }).
		AddButton("登录", func() {
			if username == "" || password == "" {
				form.SetTitle("账户或密码不能为空")
				return
			}
			app.SetRoot(stdout, true)
			slog.Info("正在进行登录中...")
			go loginYY(username, password, app)
		}).
		AddButton("退出", func() { app.Stop() })
	if err := app.SetRoot(form, true).EnableMouse(true).Run(); err != nil {
		panic(err)
	}
}

func loginYY(username, password string, app *tview.Application) {
	yyClient := vipyy.NewClient(vipyy.WithHTTPClient(httpClient))
	preLoginRet, err := yyClient.PreLogin()
	if err != nil {
		slog.Error("加载登录参数失败", "err", err)
		return
	}
	// 需要获取oauthToken, 后续步骤要用到
	u, err := url2.Parse(preLoginRet.Url)
	if err != nil {
		slog.Error("解析登录链接失败", "err", err)
		return
	}
	oauthToken := u.Query().Get("oauth_token")
	if oauthToken == "" {
		slog.Error("获取oauthToken失败", "url", preLoginRet.Url)
		return
	}
	yyClient.GetHTTPClient().Get(preLoginRet.Url).Do() // 请求一下登录链接, 因为它会返回cookie, 后续步骤要用到

	encryptedPwd, err := vipyy.EncryptPassword(context.Background(), password)
	if err != nil {
		slog.Error("YY密码加密失败", "err", err)
		return
	}

	var passCaptcha bool
	loginPayload := vipyy.ReqLoginAsyn{
		Username:   username,
		PwdEncrypt: encryptedPwd,
		OAuthToken: oauthToken,
	}
login:
	loginAsynRet, err := yyClient.LoginAsyn(loginPayload)
	if err != nil {
		if !errors.As(err, &loginAsynRet) || loginAsynRet.Code != "1000001" {
			slog.Error("登录失败", "err", err)
			return
		}
		// 只处理需要进一步验证的情况
		if loginAsynRet.Obj.Itvjs != "" { // 验证码验证
			if passCaptcha {
				slog.Error("已经通过验证码后再次登录依旧出现验证码", "err", err)
				return
			}
			// 获取captchaId和验证码类型
			matchRet := regexp.MustCompile(`captchaId:"(.*?)"`).FindStringSubmatch(loginAsynRet.Obj.Itvjs)
			if len(matchRet) < 2 {
				slog.Error("获取captchaId失败", "itvjs", loginAsynRet.Obj.Itvjs)
				return
			}
			captchaId := matchRet[1]
			var captchaType baiduCaptchaV2Resolve.CaptchaType
			switch {
			case strings.Contains(loginAsynRet.Obj.Itvjs, "verify.baidu.spin.v2.min.js"):
				captchaType = baiduCaptchaV2Resolve.CaptchaTypeSpin
			default:
				slog.Error("不支持的验证码类型", "itvjs", loginAsynRet.Obj.Itvjs)
				return
			}
			// 通过验证
			ymTokenChan := make(chan string)
			app.QueueUpdateDraw(func() {
				inputField := tview.NewInputField().SetLabel("云码Token").SetFieldWidth(25)
				inputField.SetDoneFunc(func(key tcell.Key) {
					token := inputField.GetText()
					if token == "" {
						inputField.SetLabel("云码Token(不可为空)")
						return
					}
					ymTokenChan <- token
					close(ymTokenChan)
					app.SetRoot(stdout, true)
				})
				app.SetRoot(inputField, true)
			})
			ymToken := <-ymTokenChan
			ymClient := jfbym.NewClient(ymToken, jfbym.WithDeveloperSecret("478e41b0b8333822eec0be8fc8daf60e"), jfbym.WithHttpClient(httpClient))
			bClient := baiduCaptchaV2Resolve.NewClient(
				"eY3liK5y39DnoASXgAPwUB8Phh4Q2NLY", "https://lgn.yy.com/",
				preLoginRet.Url, ymClient, baiduCaptchaV2Resolve.WithHttpClient(httpClient),
				baiduCaptchaV2Resolve.WithMaxAttempt(5),
			)
			captchaRet, err := bClient.Do(captchaType)
			if err != nil {
				if target := new(baiduCaptchaV2Resolve.RespLog); errors.As(err, &target) && target.Op == 3 {
					slog.Error("云码识别验证码失败了")
					return
				}
				slog.Error("验证绕过失败", "err", err)
				return
			}
			baiduV2SubmitRet, err := yyClient.BaiduV2Submit(captchaId, captchaRet.Ds, captchaRet.Tk, "7")
			if err != nil {
				slog.Error("提交验证码失败", "err", err)
				return
			}
			loginPayload.VK = loginAsynRet.Obj.Vk
			loginPayload.VV = baiduV2SubmitRet.Obj
			passCaptcha = true
			goto login
		} else if loginAsynRet.Obj.RedirectURL != "" { // 其他验证, 可能是手机验证码或者其他之类的
			// 输出对应的验证页面, 以便检查具体验证内容
			u, err := url2.Parse(loginAsynRet.Obj.RedirectURL)
			if err != nil {
				slog.Error("登录失败, 遇到进一步验证, 验证类型为其他类型, 进一步查询类型时, 无法解析跳转的url", "url", loginAsynRet.Obj.RedirectURL)
				return
			}
			if u.Scheme == "" {
				u.Scheme = "https"
			}
			if u.Host == "" {
				u.Host = "lgn.yy.com"
			}
			yyClient.GetHTTPClient().Post(u.String()).SetFormData(map[string]string{
				"verifyid":        loginAsynRet.Obj.Verifyid,
				"qin":             loginAsynRet.Obj.Qin,
				"denyCallbackURL": "https://vip.yy.com/service/web/auth/fail",
				"evtCallback":     "",
				"UIStyle":         "xelogin",
				"customizeCssUrl": "",
				"cssid":           "5034",
				"isRemMe":         "0",
				"oauth_token":     oauthToken,
			}).SetOutput(os.Stdout).Do()
			slog.Error("YY登录失败, 登录过程遇到验证, 该验证并非验证码类型, 具体情况请自行分析上面输出的内容.")
			return
		}
	}
	u, err = url2.ParseRequestURI(loginAsynRet.Obj.CallbackURL)
	if err != nil {
		slog.Error("解析登录成功后的回调链接失败", "url", loginAsynRet.Obj.CallbackURL)
		return
	}
	hc := yyClient.GetHTTPClient()
	jar := hc.GetClient().Jar
	jar.SetCookies(u, []*http.Cookie{
		{
			Name:   "udboauthtmptokensec",
			Value:  preLoginRet.Ttokensec,
			Path:   "/",
			Domain: u.Host,
		},
	})
	resp := hc.Get(loginAsynRet.Obj.CallbackURL).SetHeader("Referer", "https://lgn.yy.com/").Do()
	if resp.Err != nil {
		panic(resp.Err)
	}
	match := regexp.MustCompile(`\(['"](.*?)['"]`).FindStringSubmatch(resp.String())
	if len(match) < 2 {
		slog.Error("找不到用于写入cookie的链接", "resp", resp.String())
		return
	}
	url := match[1]
	resp = hc.Get(url).SetHeader("Referer", "https://vip.yy.com/").Do()
	if resp.Err != nil {
		slog.Error("获取登录后的cookie失败", "err", resp.Err)
	}
	cookies := jar.Cookies(&url2.URL{Scheme: "https", Host: "yy.com", Path: "/"})
	var uOK, pOK bool
	for _, cookie := range cookies {
		if !uOK && strings.EqualFold(cookie.Name, "username") {
			uOK = true
		} else if !pOK && strings.EqualFold(cookie.Name, "password") {
			pOK = true
		} else if uOK && pOK {
			break
		}
	}
	cookieSB := &strings.Builder{}
	for _, cookie := range cookies {
		if cookie.Quoted {
			cookieSB.WriteString(fmt.Sprintf("%s=%q; ", cookie.Name, cookie.Value))
		} else {
			cookieSB.WriteString(fmt.Sprintf("%s=%s; ", cookie.Name, cookie.Value))
		}
	}
	if !uOK || !pOK {
		slog.Error("登录失败了, cookie不正常", "cookie", cookieSB.String())
		return
	}
	slog.Info("登录成功", "cookie", cookieSB.String())
}
